From a9e2c35e4471623a19c4b786170d751d0cc7e995 Mon Sep 17 00:00:00 2001 From: tsteven4 <13596209+tsteven4@users.noreply.github.com> Date: Tue, 26 Oct 2021 13:49:48 -0600 Subject: [PATCH] Add support for Qt6. (#739) * Add support for Qt6. * fix a few codacy complaints. --- .github/workflows/macos.yml | 43 ++++++++++ .github/workflows/windows.yml | 26 ++++-- CMakeLists.txt | 24 ++++-- GPSBabel.pro | 5 ++ csv_util.cc | 5 +- defs.h | 4 +- garmin_tables.cc | 2 +- gui/CMakeLists.txt | 18 ++-- gui/app.pro | 2 +- ignrando.cc | 51 +++++++++-- inifile.cc | 4 + lowranceusr.h | 2 +- maggeo.cc | 10 ++- nmea.cc | 2 +- pcx.cc | 8 +- shape.cc | 6 +- src/core/codecdevice.cc | 157 ++++++++++++++++++++++++++++++++++ src/core/codecdevice.h | 62 ++++++++++++++ src/core/logging.h | 11 +++ src/core/textstream.cc | 81 ++++++++++++++++-- src/core/textstream.h | 26 ++++-- tools/build_extra_tests | 2 +- tools/build_qt6.sh | 136 +++++++++++++++++++++++++++++ tools/ci_install_qt.sh | 48 +++++++++++ tools/travis_script_osx | 14 ++- util.cc | 6 +- xcsv.cc | 4 + 27 files changed, 700 insertions(+), 59 deletions(-) create mode 100644 src/core/codecdevice.cc create mode 100644 src/core/codecdevice.h create mode 100755 tools/build_qt6.sh create mode 100755 tools/ci_install_qt.sh diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 496397364..b814290fa 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -85,3 +85,46 @@ jobs: source ${HOME}/Cache/qt-${QT_VERSION}.env sudo xcode-select --switch /Applications/Xcode_12.1.1.app ./tools/travis_script_osx + + macos_qt6: + name: macos Qt6 Build + runs-on: macos-11 + env: + QT_VERSION: '6.2.0' + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Cache Qt + uses: actions/cache@v2 + id: cache + with: + path: ~/Cache + key: ${{ runner.os }}-${{ env.QT_VERSION }}-${{ secrets.CACHE_VERSION }} + + - name: Qt install setup + if: steps.cache.outputs.cache-hit != 'true' + uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Qt install + if: steps.cache.outputs.cache-hit != 'true' + run: | + pip3 install aqtinstall>=2.0.0 + ./tools/ci_install_qt.sh mac ${QT_VERSION} clang_64 ${HOME}/Cache/Qt + echo "export PATH=${HOME}/Cache/Qt/${QT_VERSION}/macos/bin:\$PATH" > "${HOME}/Cache/qt-${QT_VERSION}.env" + + - name: Script + run: | + source ${HOME}/Cache/qt-${QT_VERSION}.env + sudo xcode-select --switch /Applications/Xcode_12.5.1.app + ./tools/travis_script_osx + + - name: 'Upload Artifacts' + uses: actions/upload-artifact@v2 + with: + name: MacOS_Installer_Qt6 + path: gui/GPSBabel-*.dmg + retention-days: 14 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 59c59811b..fa7387c04 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -49,12 +49,13 @@ jobs: COMPILER: 'msvc2017' RELEASE: false FLOW: 'nmake' - - QT_VERSION: '5.12.10' - ARCH: 'x86' - HOST_ARCH: 'x86' - COMPILER: 'msvc2017' + - QT_VERSION: '6.2.0' + ARCH: 'amd64' + HOST_ARCH: 'amd64' + COMPILER: 'msvc2019_64' + AQTARCH: 'win64_msvc2019_64' RELEASE: false - FLOW: 'msbuild' + FLOW: 'nmake' steps: - name: Checkout repository @@ -62,11 +63,26 @@ jobs: - name: Cache Qt uses: actions/cache@v2 + id: cache with: path: ~/Cache key: ${{ runner.os }}-${{ matrix.QT_VERSION }}-${{ matrix.COMPILER }}-${{ secrets.CACHE_VERSION }} + - name: Install Qt setup(aqt) + if: ${{ (steps.cache.outputs.cache-hit != 'true') && startsWith(matrix.QT_VERSION, '6') }} + uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Install Qt(aqt) + if: ${{ (steps.cache.outputs.cache-hit != 'true') && startsWith(matrix.QT_VERSION, '6') }} + shell: bash + run: | + pip3 install aqtinstall>=2.0.0 + ./tools/ci_install_qt.sh windows ${{ matrix.QT_VERSION }} ${{ matrix.AQTARCH }} ${HOME}/Cache/Qt + - name: Install Qt + if: ${{ (steps.cache.outputs.cache-hit != 'true') && !startsWith(matrix.QT_VERSION, '6') }} env: CI_BUILD_DIR: ${{ github.workspace }} shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index 65358f702..cfaf4a65c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,12 +13,18 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) # Find includes in corresponding build directories set(CMAKE_INCLUDE_CURRENT_DIR ON) -# Find the Qt5Core library -find_package(Qt5 COMPONENTS Core REQUIRED) -if(${Qt5Core_VERSION} VERSION_LESS 5.12) - message(FATAL_ERROR "Qt version ${Qt5Core_VERSION} found, but version 5.9 or newer is required.") +# Find the QtCore library +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED) +list(APPEND QT_LIBRARIES Qt${QT_VERSION_MAJOR}::Core) +if(${Qt${QT_VERSION_MAJOR}Core_VERSION} VERSION_LESS 5.12) + message(FATAL_ERROR "Qt version ${Qt${QT_VERSION_MAJOR}Core_VERSION} found, but version 5.12 or newer is required.") else() - message(STATUS "Using Qt5 version ${Qt5Core_VERSION}") + message(STATUS "Using Qt${QT_VERSION_MAJOR} version ${Qt${QT_VERSION_MAJOR}Core_VERSION}") +endif() +if(${QT_VERSION_MAJOR} EQUAL "6") + find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core5Compat REQUIRED) + list(APPEND QT_LIBRARIES Qt${QT_VERSION_MAJOR}::Core5Compat) endif() set(MINIMAL_FMTS @@ -94,6 +100,9 @@ set(SUPPORT src/core/usasciicodec.cc src/core/xmlstreamwriter.cc ) +if(${QT_VERSION_MAJOR} EQUAL "6") + set(SUPPORT ${SUPPORT} src/core/codecdevice.cc) +endif() set(HEADERS cet.h @@ -183,6 +192,9 @@ set(HEADERS src/core/xmlstreamwriter.h src/core/xmltag.h ) +if(${QT_VERSION_MAJOR} EQUAL "6") + set(HEADERS ${HEADERS} src/core/codecdevice.h) +endif() string(REPLACE .cc .h FILTER_HEADERS "${FILTERS}") set(HEADERS ${HEADERS} ${FILTER_HEADERS}) @@ -268,7 +280,7 @@ add_definitions(-DSHAPELIB_ENABLED) add_definitions(-DCSVFMTS_ENABLED) add_executable(gpsbabel ${SOURCES} ${HEADERS}) -target_link_libraries(gpsbabel Qt5::Core ${LIBS}) +target_link_libraries(gpsbabel ${QT_LIBRARIES} ${LIBS}) message(STATUS "Sources are: \"${SOURCES}\"") message(STATUS "Headers are: \"${HEADERS}\"") diff --git a/GPSBabel.pro b/GPSBabel.pro index 5627a7631..1bbb8d0cf 100644 --- a/GPSBabel.pro +++ b/GPSBabel.pro @@ -14,6 +14,7 @@ if(equals(QT_MAJOR_VERSION, $$MIN_QT_VERSION_MAJOR):equals(QT_MINOR_VERSION, $$M } QT -= gui +versionAtLeast(QT_VERSION, 6.0): QT += core5compat TARGET = gpsbabel VERSION = 1.7.0 @@ -110,6 +111,8 @@ SUPPORT = route.cc waypt.cc filter_vecs.cc util.cc vecs.cc mkshort.cc \ src/core/usasciicodec.cc \ src/core/xmlstreamwriter.cc +versionAtLeast(QT_VERSION, 6.0): SUPPORT += src/core/codecdevice.cc + HEADERS = \ cet.h \ cet_util.h \ @@ -186,6 +189,8 @@ HEADERS = \ src/core/xmlstreamwriter.h \ src/core/xmltag.h +versionAtLeast(QT_VERSION, 6.0): HEADERS += src/core/codecdevice.h + HEADERS += $$FILTER_HEADERS win32-msvc* { diff --git a/csv_util.cc b/csv_util.cc index 33128e756..8233533b8 100644 --- a/csv_util.cc +++ b/csv_util.cc @@ -31,7 +31,6 @@ #include // for QDebug #include // for QRegularExpression #include // for QString, operator+ -#include // for QStringRef #include "defs.h" #include "csv_util.h" @@ -372,7 +371,7 @@ csv_linesplit(const QString& string, const QString& delimited_by, const int sp = p; while (p < string.size() && !dfound) { - if ((elen > 0) && string.midRef(p).startsWith(enclosed_in)) { + if ((elen > 0) && string.mid(p).startsWith(enclosed_in)) { efound = true; p += elen; enclosed = !enclosed; @@ -380,7 +379,7 @@ csv_linesplit(const QString& string, const QString& delimited_by, } if (!enclosed) { - if ((dlen > 0) && string.midRef(p).startsWith(delimiter)) { + if ((dlen > 0) && string.mid(p).startsWith(delimiter)) { dfound = true; } else if (hyper_whitespace_delimiter && string.at(p).isSpace()) { dfound = true; diff --git a/defs.h b/defs.h index a9c43b29b..70ee1262a 100644 --- a/defs.h +++ b/defs.h @@ -580,6 +580,7 @@ public: using QList::count; // a.k.a. size() using QList::crbegin; using QList::crend; + using QList::detach; // silence Qt6 foreach warnings using QList::empty; // a.k.a. isEmpty() using QList::end; using QList::front; // a.k.a. first() @@ -758,6 +759,7 @@ public: using QList::count; // a.k.a. size() using QList::crbegin; using QList::crend; + using QList::detach; // silence Qt6 foreach warnings using QList::empty; // a.k.a. isEmpty() using QList::end; using QList::front; // a.k.a. first() @@ -1080,7 +1082,7 @@ case_ignore_strcmp(const QString& s1, const QString& s2) // In 95% of the callers, this could be s1.startsWith(s2)... inline int case_ignore_strncmp(const QString& s1, const QString& s2, int n) { - return s1.leftRef(n).compare(s2.left(n), Qt::CaseInsensitive); + return s1.left(n).compare(s2.left(n), Qt::CaseInsensitive); } int str_match(const char* str, const char* match); diff --git a/garmin_tables.cc b/garmin_tables.cc index 3220f20bc..a0f1c3e97 100644 --- a/garmin_tables.cc +++ b/garmin_tables.cc @@ -480,7 +480,7 @@ int gt_find_icon_number_from_desc(const QString& desc, garmin_formats_e garmin_f base = 7680; } if (base) { - n = desc.midRef(7).toInt(); + n = desc.mid(7).toInt(); return n + base; } } diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index aaf5548cf..3d0c77b0e 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -19,18 +19,18 @@ set(CMAKE_AUTOUIC ON) # Handle the Qt rcc code generator automatically set(CMAKE_AUTORCC ON) -# Find the Qt5Core library -find_package(Qt5 COMPONENTS Core Gui Network Widgets Xml REQUIRED) -list(APPEND QT_LIBRARIES Qt5::Core Qt5::Gui Qt5::Network Qt5::Widgets Qt5::Xml) -if(${Qt5Core_VERSION} VERSION_LESS 5.12) - message(FATAL_ERROR "Qt version ${Qt5Core_VERSION} found, but version 5.9 or newer is required.") +# Find the QtCore library +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Network Widgets Xml REQUIRED) +list(APPEND QT_LIBRARIES Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Xml) +if(${Qt${QT_VERSION_MAJOR}Core_VERSION} VERSION_LESS 5.12) + message(FATAL_ERROR "Qt version ${Qt${QT_VERSION_MAJOR}Core_VERSION} found, but version 5.12 or newer is required.") else() - message(STATUS "Using Qt5 version ${Qt5Core_VERSION}") + message(STATUS "Using Qt${QT_VERSION_MAJOR} version ${Qt${QT_VERSION_MAJOR}Core_VERSION}") endif() -# hard code webengine instead of webkit for now -find_package(Qt5 COMPONENTS WebEngineWidgets WebChannel REQUIRED) -list(APPEND QT_LIBRARIES Qt5::WebEngineWidgets Qt5::WebChannel) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS WebEngineWidgets WebChannel REQUIRED) +list(APPEND QT_LIBRARIES Qt${QT_VERSION_MAJOR}::WebEngineWidgets Qt${QT_VERSION_MAJOR}::WebChannel) if(APPLE) find_library(IOKIT_LIBRARIES IOKit) diff --git a/gui/app.pro b/gui/app.pro index 2746f05ce..66e804d62 100755 --- a/gui/app.pro +++ b/gui/app.pro @@ -1,7 +1,7 @@ # $Id: app.pro,v 1.19 2010-11-01 03:30:42 robertl Exp $ # -CONFIG += qt +#CONFIG += qt causes link failure on msvc. Qt6EntryPoint.lib not added to libs. CONFIG(debug, debug|release) { CONFIG += console } diff --git a/ignrando.cc b/ignrando.cc index f5e5b3855..fdda2e7bd 100644 --- a/ignrando.cc +++ b/ignrando.cc @@ -19,13 +19,30 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +#include // for sscanf +#include // for atoi +#include // for strftime, localtime, time_t, tm + +#include // for QByteArray +#include // for QIODevice, QIODeviceBase::ReadOnly +#include // for QList +#include // for QString, operator== +#include // for QXmlStreamAttributes +#include // for QT_VERSION, QT_VERSION_CHECK, qPrintable + #include "defs.h" -#include "xmlgeneric.h" -#include -#include +#include "gbfile.h" // for gbfprintf, gbfclose, gbfopen, gbfile +#include "src/core/datetime.h" // for DateTime +#include "src/core/file.h" // for File +#include "xmlgeneric.h" // for xg_callback, xg_string, cb_cdata, xml_deinit, xml_init, xml_readunicode, cb_start, cb_end, xg_cb_type, xg_tag_mapping + #define MYNAME "IGNRando" +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) +static QString rd_fname; +#endif static gbfile* fout; static route_head* track; @@ -114,7 +131,7 @@ ignr_etape_pos(xg_string args, const QXmlStreamAttributes*) { ignr_xml_error((wpt == nullptr) || (args.isEmpty())); - if (2 != sscanf(CSTRc(args), "%lf,%lf", &wpt->latitude, &wpt->longitude)) { + if (2 != sscanf(STRFROMUNICODE(args), "%lf,%lf", &wpt->latitude, &wpt->longitude)) { fatal(MYNAME ": Invalid coordinates \"%s\"!\n", qPrintable(args)); } } @@ -127,7 +144,7 @@ ignr_etape_alt(xg_string args, const QXmlStreamAttributes*) return; } - if (1 != sscanf(CSTRc(args), "%lf", &wpt->altitude)) { + if (1 != sscanf(STRFROMUNICODE(args), "%lf", &wpt->altitude)) { fatal(MYNAME ": Invalid altitude \"%s\"!\n", qPrintable(args)); } } @@ -137,7 +154,12 @@ ignr_etape_alt(xg_string args, const QXmlStreamAttributes*) static void ignr_rd_init(const QString& fname) { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) xml_init(fname, ignr_xml_map, nullptr); +#else + rd_fname = fname; + xml_init(nullptr, ignr_xml_map, nullptr); +#endif wpt = nullptr; track = nullptr; } @@ -146,12 +168,29 @@ static void ignr_rd_deinit() { xml_deinit(); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + rd_fname.clear(); +#endif } static void ignr_read() { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + // QXmlStreamReader had access to the windows-1252 QTextCodec that we expect + // to find in the XMLDecl. xml_read(); +#else + // QXmlStreamReader doesn't have access to a windows-1252 QStringDecoder, + // and will throw an error if we pass a QIODevice when it sees windows-1252 + // in the XMLDecl. + // Therfore we must decode the input manually and pass a QString. + // With a QString QXmlStreamReader will ignore the XMLDecl. + gpsbabel::File file(rd_fname); + file.open(QIODevice::ReadOnly); + xml_readunicode(STRTOUNICODE(file.readAll())); + file.close(); +#endif } /* write support */ @@ -182,7 +221,7 @@ ignr_write_track_hdr(const route_head* track_hdr) gbfprintf(fout, "\t\n"); gbfprintf(fout, "\t\t%d\n", track_hdr->rte_waypt_ct); if (!track_hdr->rte_desc.isEmpty()) { - gbfprintf(fout, "\t\t%s\n", CSTRc(track_hdr->rte_desc)); + gbfprintf(fout, "\t\t%s\n", STRFROMUNICODE(track_hdr->rte_desc)); } gbfprintf(fout, "\t\n"); } diff --git a/inifile.cc b/inifile.cc index 4f22fdce3..404891cd1 100644 --- a/inifile.cc +++ b/inifile.cc @@ -185,7 +185,11 @@ inifile_init(const QString& filename, const char* myname) gpsbabel::File file(name); file.open(QFile::ReadOnly); QTextStream stream(&file); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + // default for QTextStream::setCodec in Qt5 is QTextCodec::codecForLocale() + // default for QTextStream::setEncoding in Qt6 is QStringConverter::Utf8 stream.setCodec("UTF-8"); +#endif stream.setAutoDetectUnicode(true); auto* result = new inifile_t; diff --git a/lowranceusr.h b/lowranceusr.h index ce082b720..249774c01 100644 --- a/lowranceusr.h +++ b/lowranceusr.h @@ -500,7 +500,7 @@ private: * Also return the icon number for descriptions of "icon-" * followed by a numeric icon number. */ - int n = desc.midRef(desc.startsWith("icon-") ? 5 : 0).toInt(); + int n = desc.mid(desc.startsWith("icon-") ? 5 : 0).toInt(); if (n) { return n; } diff --git a/maggeo.cc b/maggeo.cc index f1b199ccd..5ede9ef20 100644 --- a/maggeo.cc +++ b/maggeo.cc @@ -191,10 +191,14 @@ maggeo_fmtdate(const QDateTime& dt) static QDateTime maggeo_parsedate(char* dmy) { QString date(dmy); - int d = date.midRef(0,2).toInt(); - int m = date.midRef(2,2).toInt(); - int y = date.midRef(4,3).toInt(); + int d = date.mid(0,2).toInt(); + int m = date.mid(2,2).toInt(); + int y = date.mid(4,3).toInt(); +#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) QDateTime r(QDate(y + 1900, m, d)); +#else + QDateTime r = QDate(y + 1900, m, d).startOfDay(); +#endif return r; } diff --git a/nmea.cc b/nmea.cc index 733e9967b..8c55f9b93 100644 --- a/nmea.cc +++ b/nmea.cc @@ -876,7 +876,7 @@ NmeaFormat::nmea_parse_one_line(const QByteArray& ibuf) if (ok) { int ckval = nmea_cksum(tbuf.mid(1, ckidx - 1)); if (ckval != ckcmp) { - Warning().nospace() << hex << "Invalid NMEA checksum. Computed 0x" << ckval << " but found 0x" << ckcmp << ". Ignoring sentence."; + Warning().nospace() << Qt::hex << "Invalid NMEA checksum. Computed 0x" << ckval << " but found 0x" << ckcmp << ". Ignoring sentence."; return; } checked = true; diff --git a/pcx.cc b/pcx.cc index 30fd51b6d..7a50015d7 100644 --- a/pcx.cc +++ b/pcx.cc @@ -182,8 +182,8 @@ static void data_read() { wpt_tmp->longitude = lon; wpt_tmp->latitude = lat; } else { - lat = tbuf.midRef(1, -1).toDouble(); - lon = nbuf.midRef(1, -1).toDouble(); + lat = tbuf.mid(1, -1).toDouble(); + lon = nbuf.mid(1, -1).toDouble(); if (tbuf[0] == 'S') { lat = -lat; } @@ -254,8 +254,8 @@ static void data_read() { wpt_tmp->longitude = lon; wpt_tmp->latitude = lat; } else { - lat = tbuf.midRef(1, -1).toDouble(); - lon = nbuf.midRef(1, -1).toDouble(); + lat = tbuf.mid(1, -1).toDouble(); + lon = nbuf.mid(1, -1).toDouble(); if (tbuf[0] == 'S') { lat = -lat; } diff --git a/shape.cc b/shape.cc index 6a11e022a..6bb81a499 100644 --- a/shape.cc +++ b/shape.cc @@ -24,7 +24,7 @@ #include // for QByteArray #include // for QLatin1String -#include // for QString, QString::SkipEmptyParts +#include // for QString, Qt::SkipEmptyParts #include // for QStringList #include // for QVector #include // for CaseInsensitive @@ -218,7 +218,11 @@ ShapeFormat::read() if (qopt_name.contains('+')) { // form a compound name from one or more fields. nameidx = -2; +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const QStringList opt_name_fields = qopt_name.split('+', QString::SkipEmptyParts); +#else + const QStringList opt_name_fields = qopt_name.split('+', Qt::SkipEmptyParts); +#endif nameindices.reserve(opt_name_fields.size()); for (int oidx=0; oidx // for memcpy +#include // for min + +#include // for QByteArray +#include // for QChar +#include // for QFile +#include // for QFlags + +#include "defs.h" // for list_codecs, warning +#include "codecdevice.h" + +namespace gpsbabel +{ + +CodecDevice::CodecDevice(const QString& fname, const char* module, const char* codec_name) : + fname_(fname), module_(module), codec_name_(codec_name) +{ +} + +CodecDevice::~CodecDevice() +{ +// close(); +} + +bool CodecDevice::open(QIODevice::OpenMode mode) +{ + codec_ = QTextCodec::codecForName(codec_name_); + if (codec_ == nullptr) { + list_codecs(); + fatal("%s: Unsupported codec '%s'.\n", module_, codec_name_); + // return false; + } + + if (mode & QIODevice::ReadOnly) { + decoder_ = codec_->makeDecoder(); + } + + if (mode & QIODevice::WriteOnly) { + encoder_ = codec_->makeEncoder(); + } + + file_ = new gpsbabel::File(fname_); + bool status = file_->open(mode); + + QIODevice::open(mode); + + return status; + +} + +qint64 CodecDevice::readData(char* data, qint64 maxlen) +{ + qint64 bytesdelivered = 0; + + while (bytesdelivered < maxlen) { + if (unicodebuffer_bytes_ > 0) { + qint64 bytes = std::min(maxlen, unicodebuffer_bytes_); + memcpy(data, unicodebuffer_data_, bytes); + unicodebuffer_bytes_ -= bytes; + unicodebuffer_data_ += bytes; + data += bytes; + bytesdelivered += bytes; + if (bytesdelivered == maxlen) { + break; + } + } + + qint64 bytesread = file_->read(charbuffer_, charbuffer_size_); + if (bytesread <= 0) { // no more data is available or error. + if (bytesdelivered > 0) { + break; + } + return -1; + } + + unicodebuffer_ = decoder_->toUnicode(charbuffer_, bytesread); + unicodebuffer_bytes_ = unicodebuffer_.size() * sizeof(QChar); + unicodebuffer_data_ = reinterpret_cast(unicodebuffer_.constData()); + } + return bytesdelivered; +} + +qint64 CodecDevice::writeData(const char* data, qint64 len) +{ + qint64 bytes_consumed = 0; + + while (bytes_consumed < len) { + qint64 bytes= std::min(len - bytes_consumed, charbuffer_bytes_free_); + memcpy(charbuffer_data_, data, bytes); + bytes_consumed += bytes; + charbuffer_data_ += bytes; + charbuffer_bytes_free_ -= bytes; + data += bytes; + + if (charbuffer_bytes_free_ == 0) { + static_assert(charbuffer_size_%sizeof(QChar) == 0); + QByteArray ba = encoder_->fromUnicode(reinterpret_cast(charbuffer_), charbuffer_size_/sizeof(QChar)); + file_->write(ba); + charbuffer_data_ = charbuffer_; + charbuffer_bytes_free_ = charbuffer_size_; + } + } + return len; +} + +void CodecDevice::close() +{ + if (charbuffer_bytes_free_ < charbuffer_size_) { + qint64 bytes = charbuffer_size_ - charbuffer_bytes_free_; + assert(bytes%sizeof(QChar) == 0); + QByteArray ba = encoder_->fromUnicode(reinterpret_cast(charbuffer_), bytes/sizeof(QChar)); + file_->write(ba); + charbuffer_data_ = charbuffer_; + charbuffer_bytes_free_ = charbuffer_size_; + } + file_->close(); + QIODevice::close(); + + if (file_ != nullptr) { + delete file_; + file_ = nullptr; + } + if (encoder_ != nullptr) { + delete encoder_; + encoder_ = nullptr; + } + if (decoder_ != nullptr) { + delete decoder_; + decoder_ = nullptr; + } +} + +bool CodecDevice::isSequential() const +{ + return true; +} + +} // namespace diff --git a/src/core/codecdevice.h b/src/core/codecdevice.h new file mode 100644 index 000000000..320d9aa90 --- /dev/null +++ b/src/core/codecdevice.h @@ -0,0 +1,62 @@ +/* + Copyright (C) 2021 Robert Lipe, robertlipe+source@gpsbabel.org + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + */ + +#include // for QIODevice +#include // for QString +#include // for QTextCodec +#include // for QTextDecoder +#include // for QTextEncoder +#include // for qint64, QIODeviceBase::OpenMode + +#include "src/core/file.h" // for File + +namespace gpsbabel +{ + +class CodecDevice : public QIODevice +{ +public: + CodecDevice(const QString& fname, const char* module, const char* codec_name); + ~CodecDevice(); + bool open(QIODevice::OpenMode mode) override; + bool isSequential() const override; + void close() override; + +private: + qint64 readData(char* data, qint64 maxlen) override; + qint64 writeData(const char* data, qint64 len) override; + +private: + QString fname_; + const char* module_; + const char* codec_name_; + gpsbabel::File* file_{nullptr}; + QTextCodec* codec_{nullptr}; + QTextDecoder* decoder_{nullptr}; + QTextEncoder* encoder_{nullptr}; + QString unicodebuffer_; + qint64 unicodebuffer_bytes_{0}; + const char* unicodebuffer_data_{nullptr}; + static constexpr qint64 charbuffer_size_ = 1024; + char charbuffer_[charbuffer_size_]; + char* charbuffer_data_{charbuffer_}; + qint64 charbuffer_bytes_free_{charbuffer_size_}; +}; + +} // namespace diff --git a/src/core/logging.h b/src/core/logging.h index d020e3093..8d9b41e9d 100644 --- a/src/core/logging.h +++ b/src/core/logging.h @@ -49,4 +49,15 @@ public: explicit FatalMsg() : QDebug(QtCriticalMsg) {} }; +/* + * Kludge any used QTextStream modifiers into Qt namespace as they are in newer + * versions of Qt. This makes source compatiblity easier. + */ +#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) +namespace Qt +{ + inline QTextStream& hex(QTextStream &s) { return ::hex(s); } + inline QTextStream& endl(QTextStream &s) { return ::endl(s); } +} +#endif #endif // gpsbabel_logging_h_included diff --git a/src/core/textstream.cc b/src/core/textstream.cc index 2a9adf5a7..49c10ba88 100644 --- a/src/core/textstream.cc +++ b/src/core/textstream.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2019 Robert Lipe, gpsbabel.org + Copyright (C) 2019-2021 Robert Lipe, gpsbabel.org This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,13 +17,27 @@ */ -#include // for QFile + +#include // for qint64, QT_VERSION, QT_VERSION_CHECK + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) +#include // for QByteArrayView +#endif +#include // for QFile #include // for QFlags -#include // for QIODevice, QIODevice::OpenMode, QIODevice::ReadOnly, QIODevice::WriteOnly +#include // for QIODevice +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) +#include // for QIODeviceBase::OpenMode +#include // for QStringConverter, QStringConverter::Utf8, QStringConverter::Encoding, QStringConverter::Utf16 +#endif -#include "defs.h" // for fatal, list_codecs +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) +#include // for optional +#endif + +#include "defs.h" // for fatal, list_codecs #include "src/core/textstream.h" -#include "src/core/file.h" // for File +#include "src/core/file.h" // for File namespace gpsbabel @@ -31,6 +45,7 @@ namespace gpsbabel void TextStream::open(const QString& fname, QIODevice::OpenMode mode, const char* module, const char* codec_name) { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) codec_ = QTextCodec::codecForName(codec_name); if (codec_ == nullptr) { list_codecs(); @@ -54,6 +69,54 @@ void TextStream::open(const QString& fname, QIODevice::OpenMode mode, const char setGenerateByteOrderMark(true); } } +#else + std::optional encoding = QStringConverter::encodingForName(codec_name); + bool use_stringconverter = encoding.has_value(); + + /* When reading autodetect unicode. + * The requested codec may not be supported by QStringConverter, + * but autodetection may switch to a converter that is. + */ + if (!use_stringconverter && (mode & QFile::ReadOnly)) { + QFile file = QFile(fname); + file.open(mode); + char data[4]; + qint64 bytesread = file.read(data, 4); + file.close(); + encoding = QStringConverter::encodingForData(QByteArrayView(data, bytesread)); + if (encoding.has_value()) { + use_stringconverter = true; + } + } + + if (use_stringconverter) { + file_ = new gpsbabel::File(fname); + file_->open(mode); + setDevice(file_); + setEncoding(encoding.value()); + + if (mode & QFile::ReadOnly) { + if (encoding.value() == QStringConverter::Utf8) { + setAutoDetectUnicode(true); + } + } + + if (mode & QFile::WriteOnly) { + // enable bom for all UTF codecs except UTF-8 + if (encoding.value() != QStringConverter::Utf8) { + setGenerateByteOrderMark(true); + } + } + } else { + device_ = new gpsbabel::CodecDevice(fname, module, codec_name); + bool status = device_->open(mode); + if (!status) { + fatal("%s: device not open %d\n", module, status); + } + setDevice(device_); + setEncoding(QStringConverter::Utf16); + } +#endif } void TextStream::close() @@ -64,7 +127,15 @@ void TextStream::close() delete file_; file_ = nullptr; } +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) codec_ = nullptr; +#else + if (device_ != nullptr) { + device_->close(); + delete device_; + device_ = nullptr; + } +#endif } } // namespace diff --git a/src/core/textstream.h b/src/core/textstream.h index a1f1c8fba..a3f524b31 100644 --- a/src/core/textstream.h +++ b/src/core/textstream.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2019 Robert Lipe, gpsbabel.org + Copyright (C) 2019-2021 Robert Lipe, gpsbabel.org This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,12 +19,22 @@ #ifndef SRC_CORE_TEXTSTREAM_INCLUDED_H_ #define SRC_CORE_TEXTSTREAM_INCLUDED_H_ -#include // for QIODevice, QIODevice::OpenMode -#include // for QString -#include // for QTextCodec -#include // for QTextStream +#include // for QT_VERSION, QT_VERSION_CHECK -#include "src/core/file.h" // for File +#include // for QIODevice +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) +#include // for QIODeviceBase::OpenMode +#endif +#include // for QString +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) +#include // for QTextCodec +#endif +#include // for QTextStream + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) +#include "src/core/codecdevice.h" // for CodecDevice +#endif +#include "src/core/file.h" // for File namespace gpsbabel @@ -38,7 +48,11 @@ public: private: gpsbabel::File* file_{nullptr}; +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QTextCodec* codec_{nullptr}; +#else + gpsbabel::CodecDevice* device_{nullptr}; +#endif }; } // namespace diff --git a/tools/build_extra_tests b/tools/build_extra_tests index 30b45cd1f..235f55518 100755 --- a/tools/build_extra_tests +++ b/tools/build_extra_tests @@ -34,7 +34,7 @@ make clean make -j 3 make check -export CLAZY_CHECKS=level0,level1,no-non-pod-global-static +export CLAZY_CHECKS=level0,level1,no-non-pod-global-static,no-qstring-ref qmake -spec linux-clang "CONFIG+=debug" "QMAKE_CXX=clazy" make clean make -j 3 2>&1 | tee clazy.log diff --git a/tools/build_qt6.sh b/tools/build_qt6.sh new file mode 100755 index 000000000..d413a7179 --- /dev/null +++ b/tools/build_qt6.sh @@ -0,0 +1,136 @@ +#!/bin/bash -ex + +if [ $# -ne 1 ]; then + echo "$0 version" + exit 1 +fi +version=${1} + +sourcetype=git +#flavor=debug +flavor=release +#flavor=debug-and-release + +buildroot=$(pwd)/qt-${version}-${flavor}-${sourcetype} +mkdir "$buildroot" +sourcedir=${buildroot}/source +builddir=${buildroot}/build +if [ "$(uname -s)" = "Darwin" ]; then + installdir=/Users/travis/Cache/Qt + compilerdir=clang_64 + archive=qt-${version}-${flavor}-macos +else + installdir=/home/travis/Cache/Qt + compilerdir=gcc_64 + archive=qt-${version}-${flavor}-linux +fi + + +if [ -e "${sourcedir}" ]; then + echo "source directory \"${sourcedir}\" already exits." + exit 1 +fi +if [ -e "${builddir}" ]; then + echo "build directory \"${builddir}\" already exits." + exit 1 +fi +if [ -e "${installdir}" ]; then + echo "install directory \"${installdir}\" already exits." + exit 1 +fi + +if [ "${sourcetype}" == "git" ]; then + git clone git://code.qt.io/qt/qt5.git "${sourcedir}" +else + mkdir -p "${sourcedir}" + versionmm=$(echo "${version}" | cut -d. -f1,2) + if [ ! -e "qt-everywhere-src-${version}.tar.xz" ]; then + wget -nv "https://download.qt.io/archive/qt/${versionmm}/${version}/single/qt-everywhere-src-${version}.tar.xz" + fi + tar -x --strip-components 1 --xz --directory "${sourcedir}" --file "qt-everywhere-src-${version}.tar.xz" +fi + +cd "${sourcedir}" + +# exclude modules without some kind of LGPL license. +# virtualkeyboard is pretty sticky, it gets deployed if it exists and you use Gui. +excludes=( \ +qtcharts \ +qtdatavis3d \ +qtlottie \ +qtnetworkauth \ +qtquick3d \ +qtvirtualkeyboard \ +qtwebglplugin \ +) + +# also some modules we don't use +excludes+=( \ +qtpurchasing \ +qtscript \ +qtxmlpatterns \ +qtactiveqt \ +qtcoap \ +qtopcua \ +qtmqtt \ +qtscxml \ +) + +# other modules we don't want +# the tarballs don't include these modules, but git does +excludes+=( \ +qtqa \ +qtscript \ +qtquickcontrols2 \ +qtwayland \ +qt3d \ +qtquicktimeline \ +qtdoc \ +) +#qtwebengine \ +#qtwebchannel \ + +# qtmultimedia requires: +#qtimageformats \ +#qtshadertools \ + +if [ "${sourcetype}" == "git" ]; then + if true; then + # tagged when released + git checkout "v${version}" + else + # branch, before tagged + git checkout "${version}" + fi + modules=essential,addon,qt5compat,-$(echo "${excludes[@]}" | sed 's/ /,-/g') + echo "$modules" + perl init-repository --module-subset="${modules}" +else + for component in "${excludes[@]}" + do + /bin/rm -fr "${component}" + done +fi +skips=$(echo "${excludes[@]}" | sed 's/[^ ]* */-skip &/g') +mkdir -p "${builddir}" +cd "${builddir}" +"${sourcedir}/configure" --prefix="${installdir}/${version}/${compilerdir}" -opensource -confirm-license -nomake examples -nomake tests "-${flavor}" "${features[@]}" ${skips} +cmake --build . --parallel +cmake --install . + +licenses=( \ +"${sourcedir}/LICENSE.FDL" \ +"${sourcedir}/LICENSE.GPLv2" \ +"${sourcedir}/LICENSE.GPLv3" \ +"${sourcedir}/LICENSE.LGPLv21" \ +"${sourcedir}/LICENSE.LGPLv3" \ +"${sourcedir}/LICENSE.QT-LICENSE-AGREEMENT" \ +) + +mkdir "${installdir}/Licenses" +for license in "${licenses[@]}" +do + cp "${license}" "${installdir}/Licenses" +done + +tar -c -C "$(dirname ${installdir})" --xz -f "${archive}.tar.xz" "$(basename ${installdir})" diff --git a/tools/ci_install_qt.sh b/tools/ci_install_qt.sh new file mode 100755 index 000000000..f56dfc251 --- /dev/null +++ b/tools/ci_install_qt.sh @@ -0,0 +1,48 @@ +#!/bin/bash -e + +# install Qt +# +# Examples: +# ci_install_qt.sh mac 6.2.0 clang_64 /tmp/Qt +# ci_install_qt.sh windows 6.2.0 win64_msvc2019_64 /tmp/Qt +# ci_install_qt.sh linux 6.2.0 gcc_64 /tmp/Qt + +host=$1 +version=$2 +arch=$3 +outdir=$4 + +available=( $(aqt list-qt "$host" desktop --modules "$version" "$arch") ) + +# remove commercial/GPLv3 modules, see https://doc-snapshots.qt.io/qt6-dev/qtmodules.html +remove=( \ +debug_info \ +qtcharts \ +qtdatavis3d \ +qtlottie \ +qtnetworkauth \ +qtquick3d \ +qtquicktimeline \ +qtwebglplugin \ +qtshadertools \ +qtvirtualkeyboard \ +qtwaylandcompositor \ +) + +mods=() +for a in "${available[@]}" +do + skip=false + for r in "${remove[@]}" + do + if [ "$a" == "$r" ]; then + skip=true + fi + done + if [ $skip == false ]; then + mods+=( "$a" ) + fi +done +echo Installing "${mods[@]}" +aqt install-qt "$host" desktop "$version" "$arch" -O "$outdir" -m "${mods[@]}" + diff --git a/tools/travis_script_osx b/tools/travis_script_osx index db91d4a55..f97a4a4ef 100755 --- a/tools/travis_script_osx +++ b/tools/travis_script_osx @@ -2,6 +2,8 @@ # # this script is run on travis for the script stage of mac builds # + +function version_ge() { test "$(printf "%s\n%s" $1 $2 | sort -rV | head -n 1)" == "$1"; } QMAKE=${QMAKE:-qmake} LUPDATE=${LUPDATE:-lupdate} @@ -14,13 +16,21 @@ VERSIONID=${VERSIONID:-$(date -ju -f %Y-%m-%dT%H:%M:%S%z $(git show -s --format= "$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)"/ci_tokens # build and test the CLI -$QMAKE GPSBabel.pro +if version_ge $($QMAKE -query QT_VERSION) 6.0.0; then + $QMAKE GPSBabel.pro QMAKE_APPLE_DEVICE_ARCHS="x86_64 arm64" +else + $QMAKE GPSBabel.pro +fi make -j 3 make check # build the GUI pushd gui -$QMAKE app.pro +if version_ge $($QMAKE -query QT_VERSION) 6.0.0; then + $QMAKE app.pro QMAKE_APPLE_DEVICE_ARCHS="x86_64 arm64" +else + $QMAKE app.pro +fi make -j 3 make package popd diff --git a/util.cc b/util.cc index 30ec114f1..7a4b4c27e 100644 --- a/util.cc +++ b/util.cc @@ -1744,8 +1744,8 @@ list_codecs() maxlen = codec->name().size(); } } - info << "Available Codecs:" << endl; - info << qSetFieldWidth(8) << "MIBenum" << qSetFieldWidth(maxlen+1) << "Name" << qSetFieldWidth(0) << "Aliases" << endl; + info << "Available Codecs:" << Qt::endl; + info << qSetFieldWidth(8) << "MIBenum" << qSetFieldWidth(maxlen+1) << "Name" << qSetFieldWidth(0) << "Aliases" << Qt::endl; for (auto mib : mibs) { auto* codec = QTextCodec::codecForMib(mib); info << qSetFieldWidth(8) << mib << qSetFieldWidth(maxlen+1) << codec->name() << qSetFieldWidth(0); @@ -1759,7 +1759,7 @@ list_codecs() } info << alias; } - info << endl; + info << Qt::endl; } } diff --git a/xcsv.cc b/xcsv.cc index 5e413ea79..4114018ee 100644 --- a/xcsv.cc +++ b/xcsv.cc @@ -258,7 +258,11 @@ QDateTime XcsvFormat::yyyymmdd_to_time(const char* s) { QDate d = QDate::fromString(s, "yyyyMMdd"); +#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) return QDateTime(d); +#else + return d.startOfDay(); +#endif } -- 2.30.2